feat: Add fix(), unfix(), and fixed to Variable and Variables#625
feat: Add fix(), unfix(), and fixed to Variable and Variables#625
Conversation
Add methods to fix variables to values via equality constraints, with: - Automatic rounding (0 decimals for int/binary, configurable for continuous) - Clipping to variable bounds to prevent infeasibility - Optional integrality relaxation (relax=True) for MILP dual extraction - Relaxed state tracked in Model._relaxed_registry and restored by unfix() - Cleanup on variable removal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Serialize the relaxed registry as a JSON string in netCDF attrs so that unfix() can restore integrality after a save/load roundtrip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FabianHofmann
left a comment
There was a problem hiding this comment.
convenient feature which does not hurt. this could be extended to fix the variables on the solver side as well. I remember the LP files allow to define fixed variables (without an explicit constraint). but this should wait after #630
linopy/variables.py
Outdated
| if value is None: | ||
| value = self.solution | ||
|
|
||
| value = DataArray(value).broadcast_like(self.labels) |
There was a problem hiding this comment.
should probably use as_dataarray here
linopy/variables.py
Outdated
|
|
||
| value = DataArray(value).broadcast_like(self.labels) | ||
|
|
||
| # Round: integers/binaries to 0 decimals, continuous to `decimals` |
There was a problem hiding this comment.
remove comments like these which explain the code
linopy/variables.py
Outdated
|
|
||
| FILL_VALUE = {"labels": -1, "lower": np.nan, "upper": np.nan} | ||
|
|
||
| FIX_CONSTRAINT_PREFIX = "__fix__" |
There was a problem hiding this comment.
move to constants.py
linopy/variables.py
Outdated
| value = value.round(decimals) | ||
|
|
||
| # Clip to bounds | ||
| value = value.clip(min=self.lower, max=self.upper) |
There was a problem hiding this comment.
I would fail-fast raise here in case values are incompatible with bounds
There was a problem hiding this comment.
The clipping is meant to prevent infeasibilities due to floating point precision. Ill check if it can be done differntly while not hiding real infeasible values.
| constraint_name = f"{FIX_CONSTRAINT_PREFIX}{self.name}" | ||
|
|
||
| # Remove existing fix constraint if present | ||
| if constraint_name in self.model.constraints: |
There was a problem hiding this comment.
add an additional argument overwrite that allows to drop existing fix constraints
There was a problem hiding this comment.
I did so. I argue that this defaults to True, as the semantics of "fixing" are pretty straight forward: fix this variable to this value. But if you disagree we can default it to false, which might catch unexpected refixing.
| # Handle integrality relaxation | ||
| if relax and (self.attrs.get("integer") or self.attrs.get("binary")): | ||
| original_type = "binary" if self.attrs.get("binary") else "integer" | ||
| self.model._relaxed_registry[self.name] = original_type |
There was a problem hiding this comment.
that is interesting, are we relaxing fixed binary and integer variable to save computation in the solving?
There was a problem hiding this comment.
This makes the problem pure LP if all binaries are fixed. My intention was to allow computing duals etc, but it might also speed up the solve, although i imagine the MILP solver to be as fast with fixed binaries.
This is the main reason for this PR, as this cannot be implemented outside of linopy easily. The equality constraints themselves could be done by a user with reasonable effort.
There was a problem hiding this comment.
Makes perfect sense!
- Use as_dataarray() instead of DataArray() for value conversion - Remove explanatory inline comments - Move FIX_CONSTRAINT_PREFIX to constants.py - Raise ValueError for out-of-bounds fix values instead of clipping - Add overwrite parameter to fix() (default True) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FabianHofmann
left a comment
There was a problem hiding this comment.
feel free to merge when you want
Changes proposed in this Pull Request
Adds
fix(),unfix(), andfixedto bothVariableand theVariablescontainer for fixing variables to values via equality constraints.Features
fix(value=None, decimals=8, relax=False)— Fixes a variable by adding a__fix__{name}equality constraint. Uses the current solution if no value is given.unfix()— Removes the fix constraint and restores integrality if it was relaxed.fixed— Property indicating whether the variable is currently fixed.decimals. Clips to variable bounds to prevent infeasibility from floating-point noise.fix(relax=True)temporarily converts integer/binary variables to continuous (tracked inModel._relaxed_registry), enabling dual extraction after re-solve.unfix()restores the original type.Model.remove_variables()automatically cleans up fix constraints and relaxed registry entries.Use cases
relax=True, re-solve as LP, read duals.Includes
manipulating-modelsnotebook.Checklist
doc.doc/release_notes.rstof the upcoming release is included.